﻿// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Azure.Provisioning.Generator.Model;

public class Resource(Specification spec, Type armType)
    : TypeModel(
        name: armType.Name.TrimSuffix("Resource")!,
        ns: spec.Namespace,
        // description: specification.DocComments.GetSummary(armType), // These aren't super meaningful doc comments so we'll avoid
        armType: armType,
        spec: spec)
{
    public string? ResourceType { get; set; }
    public string? ResourceNamespace { get; set; }
    public string? DefaultResourceVersion { get; set; }
    public IList<string>? ResourceVersions { get; set; }
    public IList<string>? HiddenResourceVersions { get; set; }
    public NameRequirements? NameRequirements { get; set; }
    public bool GenerateRoleAssignment { get; set; } = false;
    public Resource? ParentResource { get; set; }
    public SimpleModel? GetKeysType { get; set; }
    public bool GetKeysIsList { get; set; }

    public override string ToString() => $"<Resource {Spec!.Name}::{Name}>";

    public override void Lint()
    {
        base.Lint();
        //if (NameRequirements is null) { Warn($"{GetTypeReference()} has no {nameof(NameRequirements)}."); }
        if (DefaultResourceVersion is null) { Warn($"{ResourceType} has no {nameof(DefaultResourceVersion)}."); }
        else if (ResourceVersions is null) { Warn($"{ResourceType} has no {nameof(ResourceVersions)}."); }
    }

    public override void Generate()
    {
        ContextualException.WithContext(
            $"Generating resource {Namespace}.{Name}",
            () =>
            {
                IndentWriter writer = new();
                writer.WriteLine("// Copyright (c) Microsoft Corporation. All rights reserved.");
                writer.WriteLine("// Licensed under the MIT License.");
                writer.WriteLine();
                writer.WriteLine("// <auto-generated/>");
                writer.WriteLine();
                writer.WriteLine("#nullable enable");
                writer.WriteLine();

                // Add the usings
                HashSet<string> namespaces = CollectNamespaces();
                if (FromExpression || GenerateRoleAssignment || GetKeysType is not null) { namespaces.Add("Azure.Provisioning.Expressions"); }
                if (FromExpression || NameRequirements is not null || GetKeysType is not null || HiddenResourceVersions is not null) { namespaces.Add("System.ComponentModel"); }
                if (GenerateRoleAssignment) { namespaces.Add("Azure.Provisioning.Authorization"); namespaces.Add("Azure.Provisioning.Roles"); }
                namespaces.Remove(Namespace!);
                foreach (string ns in namespaces.Order())
                {
                    writer.WriteLine($"using {ns};");
                }

                writer.WriteLine();
                writer.WriteLine($"namespace {Namespace};");
                writer.WriteLine();
                writer.WriteLine($"/// <summary>");
                writer.WriteWrapped(Description ?? (Name + "."));
                writer.WriteLine($"/// </summary>");
                writer.WriteLine($"public partial class {Name} : {(BaseType is not null ? BaseType.Name : "ProvisionableResource")}");
                using (writer.Scope("{", "}"))
                {
                    var fence = new IndentWriter.Fenceposter();

                    // Write the properties
                    foreach (Property property in Properties)
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }
                        if (!property.HideAccessors)
                        {
                            writer.WriteLine($"/// <summary>");
                            string orSets = property.IsReadOnly ? "" : " or sets";
                            writer.WriteWrapped(property.Description ?? $"Gets{orSets} the {property.Name}.");
                            writer.WriteLine($"/// </summary>");
                            writer.WriteLine($"public {property.BicepTypeReference} {property.Name} ");
                            using (writer.Scope("{", "}"))
                            {
                                writer.WriteLine($"get {{ Initialize(); return {property.FieldName}!; }}");
                                if (!property.IsReadOnly)
                                {
                                    writer.Write($"set {{ Initialize(); ");
                                    if (property.PropertyType is SimpleModel || property.PropertyType is Resource)
                                    {
                                        writer.Write($"AssignOrReplace(ref {property.FieldName}, value);");
                                    }
                                    else
                                    {
                                        writer.Write($"{property.FieldName}!.Assign(value);");
                                    }
                                    writer.WriteLine($" }}");
                                }
                            }
                        }
                        writer.WriteLine($"private {property.BicepTypeReference}? {property.FieldName};");
                    }

                    if (ParentResource is not null)
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }
                        writer.WriteLine($"/// <summary>");
                        writer.WriteLine($"/// Gets or sets a reference to the parent {ParentResource.Name}.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"public {ParentResource.Name}? Parent");
                        using (writer.Scope("{", "}"))
                        {
                            writer.WriteLine($"get {{ Initialize(); return _parent!.Value; }}");
                            writer.WriteLine($"set {{ Initialize(); _parent!.Value = value; }}");
                        }
                        writer.WriteLine($"private ResourceReference<{ParentResource.Name}>? _parent;");
                    }

                    // Write the default value partial methods
                    foreach (Property property in Properties.Where(p => p.GenerateDefaultValue))
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }

                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Get the default value for the {property.Name} property.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"private partial {property.BicepTypeReference} Get{property.Name}DefaultValue();");
                    }

                    // Write the .ctor
                    if (fence.RequiresSeparator) { writer.WriteLine(); }
                    writer.WriteLine($"/// <summary>");
                    writer.WriteWrapped($"Creates a new {Name}.");
                    writer.WriteLine($"/// </summary>");
                    writer.WriteLine($"/// <param name=\"bicepIdentifier\">");
                    writer.WriteWrapped($"The the Bicep identifier name of the {Name} resource.  This can be used to refer to the resource in expressions, but is not the Azure name of the resource.  This value can contain letters, numbers, and underscores.");
                    writer.WriteLine($"/// </param>");
                    writer.WriteLine($"/// <param name=\"resourceVersion\">Version of the {Name}.</param>");
                    writer.WriteLine($"public {Name}(string bicepIdentifier, string? resourceVersion = default)");
                    if (BaseType is not null)
                    {
                        writer.Write($"    : base(bicepIdentifier, resourceVersion");
                    }
                    else
                    {
                        writer.Write($"    : base(bicepIdentifier, \"{ResourceType}\", resourceVersion");
                    }
                    if (DefaultResourceVersion is not null)
                    {
                        writer.Write($" ?? \"{DefaultResourceVersion}\"");
                    }
                    writer.WriteLine(")");
                    using (writer.Scope("{", "}")) { }

                    // Write the properties
                    if (fence.RequiresSeparator) { writer.WriteLine(); }
                    writer.WriteLine($"/// <summary>");
                    writer.WriteWrapped($"Define all the provisionable properties of {Name}.");
                    writer.WriteLine($"/// </summary>");
                    writer.WriteLine($"protected override void DefineProvisionableProperties()");
                    using (writer.Scope("{", "}"))
                    {
                        writer.WriteLine("base.DefineProvisionableProperties();");
                        if (DiscriminatorName is not null)
                        {
                            writer.WriteLine($"DefineProperty<string>(\"{DiscriminatorName}\", [\"{DiscriminatorName}\"], defaultValue: \"{DiscriminatorValue}\");");
                        }
                        foreach (Property property in Properties)
                        {
                            writer.Write($"{property.FieldName} = ");
                            if (property.PropertyType is SimpleModel || property.PropertyType is Resource)
                            {
                                writer.Write($"DefineModelProperty");
                            }
                            else if (property.PropertyType is ListModel lst)
                            {
                                writer.Write($"DefineListProperty");
                            }
                            else if (property.PropertyType is DictionaryModel dict)
                            {
                                writer.Write($"DefineDictionaryProperty");
                            }
                            else
                            {
                                writer.Write($"DefineProperty");
                            }
                            writer.Write($"<{property.BicepPropertyTypeReference}>(\"{property.Name}\", ");
                            writer.Write($"[{string.Join(", ", (property.Path ?? [property.Name]).Select(s => $"\"{s}\""))}]");
                            if (property.PropertyType is Resource r)
                            {
                                writer.Write($", new {r.Name}(\"{r.Name.ToCamelCase()}\")");
                            }
                            if (property.IsRequired) { writer.Write($", isRequired: true"); }
                            if (property.IsReadOnly) { writer.Write($", isOutput: true"); }
                            if (property.IsSecure) { writer.Write($", isSecure: true"); }
                            if (property.GenerateDefaultValue) { writer.Write($", defaultValue: Get{property.Name}DefaultValue()"); }
                            if (property.Format is not null) { writer.Write($", format: \"{property.Format}\""); }
                            writer.WriteLine($");");
                        }
                        if (GeneratePartialPropertyDefinition)
                        {
                            writer.WriteLine("DefineAdditionalProperties();");
                        }
                        if (ParentResource is not null)
                        {
                            writer.WriteLine($"_parent = DefineResource<{ParentResource.Name}>(\"Parent\", [\"parent\"], isRequired: true);");
                        }
                    }

                    if (GeneratePartialPropertyDefinition)
                    {
                        writer.WriteLine();
                        writer.WriteLine("private partial void DefineAdditionalProperties();");
                    }

                    // Add the well known versions
                    if (ResourceVersions is not null)
                    {
                        fence = new IndentWriter.Fenceposter();
                        writer.WriteLine();
                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Supported {Name} resource versions.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"public static class ResourceVersions");
                        using (writer.Scope("{", "}"))
                        {
                            foreach (string version in ResourceVersions)
                            {
                                if (fence.RequiresSeparator) { writer.WriteLine(); }

                                string name = $"V{version.Replace("-", "_")}";
                                writer.WriteLine($"/// <summary>");
                                writer.WriteLine($"/// {version}.");
                                writer.WriteLine($"/// </summary>");
                                writer.WriteLine($"public static readonly string {name} = \"{version}\";");
                            }

                            if (HiddenResourceVersions is not null)
                            {
                                foreach (string version in HiddenResourceVersions)
                                {
                                    if (fence.RequiresSeparator) { writer.WriteLine(); }

                                    string name = $"V{version.Replace("-", "_")}";
                                    writer.WriteLine($"/// <summary>");
                                    writer.WriteLine($"/// {version}.");
                                    writer.WriteLine($"/// </summary>");
                                    writer.WriteLine($"[EditorBrowsable(EditorBrowsableState.Never)]");
                                    writer.WriteLine($"public static readonly string {name} = \"{version}\";");
                                }
                            }
                        }
                    }

                    // Add the FromExisting method
                    if (Properties.Any(p => p.Name == "Name" && p.PropertyType?.Name == "String"))
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }

                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Creates a reference to an existing {Name}.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"/// <param name=\"bicepIdentifier\">");
                        writer.WriteWrapped($"The the Bicep identifier name of the {Name} resource.  This can be used to refer to the resource in expressions, but is not the Azure name of the resource.  This value can contain letters, numbers, and underscores.");
                        writer.WriteLine($"/// </param>");
                        writer.WriteLine($"/// <param name=\"resourceVersion\">Version of the {Name}.</param>");
                        writer.WriteLine($"/// <returns>The existing {Name} resource.</returns>");
                        writer.WriteLine($"public static {Name} FromExisting(string bicepIdentifier, string? resourceVersion = default) =>");
                        using (writer.Scope())
                        {
                            writer.WriteLine($"new(bicepIdentifier, resourceVersion) {{ IsExistingResource = true }};");
                        }
                    }

                    // Add the name requirements
                    if (NameRequirements is not null)
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }

                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Get the requirements for naming this {Name} resource.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"/// <returns>Naming requirements.</returns>");
                        writer.WriteLine($"[EditorBrowsable(EditorBrowsableState.Never)]");
                        writer.WriteLine($"public override ResourceNameRequirements GetResourceNameRequirements() =>");
                        using (writer.Scope())
                        {
                            writer.Write($"new(minLength: {NameRequirements.Min}, maxLength: {NameRequirements.Max}, validCharacters: ");
                            List<string> cases = [];
                            if (NameRequirements.Lower) { cases.Add("LowercaseLetters"); }
                            if (NameRequirements.Upper) { cases.Add("UppercaseLetters"); }
                            if (NameRequirements.Numbers) { cases.Add("Numbers"); }
                            if (NameRequirements.Hyphen) { cases.Add("Hyphen"); }
                            if (NameRequirements.Underscore) { cases.Add("Underscore"); }
                            if (NameRequirements.Period) { cases.Add("Period"); }
                            if (NameRequirements.Parens) { cases.Add("Parentheses"); }
                            writer.Write(string.Join(" | ", cases.Select(c => $"ResourceNameCharacters.{c}")));
                            writer.WriteLine($");");
                        }
                    }

                    if (GetKeysType is not null)
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }
                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Get access keys for this {Name} resource.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"/// <returns>The keys for this {Name} resource.</returns>");
                        string keyType = GetKeysType.Name;
                        if (GetKeysIsList) { keyType = $"BicepList<{keyType}>"; }
                        string expr = $"new FunctionCallExpression(new MemberExpression(new IdentifierExpression(BicepIdentifier), \"listKeys\"))";
                        writer.WriteLine($"public {keyType} GetKeys()");
                        using (writer.Scope("{", "}"))
                        {
                            if (GetKeysIsList)
                            {
                                writer.WriteLine($"return {keyType}.FromExpression(");
                                using (writer.Scope())
                                {
                                    writer.WriteLine($"e => {{ {GetKeysType.Name} key = new(); ((IBicepValue)key).Expression = e; return key; }},");
                                    writer.WriteLine($"new MemberExpression({expr}, \"keys\"));");
                                }
                            }
                            else
                            {
                                writer.WriteLine($"{keyType} key = new();");
                                writer.WriteLine($"((IBicepValue)key).Expression = {expr};");
                                writer.WriteLine($"return key;");
                            }
                        }
                    }

                    // Add the role assignment
                    if (GenerateRoleAssignment)
                    {
                        if (fence.RequiresSeparator) { writer.WriteLine(); }
                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Creates a role assignment for a user-assigned identity that grants access to this {Name}.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"/// <param name=\"role\">The role to grant.</param>");
                        writer.WriteLine($"/// <param name=\"identity\">The <see cref=\"UserAssignedIdentity\"/>.</param>");
                        writer.WriteLine($"/// <returns>The <see cref=\"RoleAssignment\"/>.</returns>");
                        writer.WriteLine($"public RoleAssignment CreateRoleAssignment({Spec!.Name}BuiltInRole role, UserAssignedIdentity identity) =>");
                        using (writer.Scope())
                        {
                            writer.WriteLine($"new($\"{{BicepIdentifier}}_{{identity.BicepIdentifier}}_{{{Spec!.Name}BuiltInRole.GetBuiltInRoleName(role)}}\")");
                            using (writer.Scope("{", "};"))
                            {
                                writer.Write($"Name = BicepFunction.CreateGuid(");
                                if (Properties.Any(p => p.Name == "Id")) { writer.Write("Id, "); }
                                writer.WriteLine($"identity.PrincipalId, BicepFunction.GetSubscriptionResourceId(\"Microsoft.Authorization/roleDefinitions\", role.ToString())),");
                                writer.WriteLine($"Scope = new IdentifierExpression(BicepIdentifier),");
                                writer.WriteLine($"PrincipalType = RoleManagementPrincipalType.ServicePrincipal,");
                                writer.WriteLine($"RoleDefinitionId = BicepFunction.GetSubscriptionResourceId(\"Microsoft.Authorization/roleDefinitions\", role.ToString()),");
                                writer.WriteLine($"PrincipalId = identity.PrincipalId");
                            }
                        }

                        if (fence.RequiresSeparator) { writer.WriteLine(); }
                        writer.WriteLine($"/// <summary>");
                        writer.WriteWrapped($"Creates a role assignment for a principal that grants access to this {Name}.");
                        writer.WriteLine($"/// </summary>");
                        writer.WriteLine($"/// <param name=\"role\">The role to grant.</param>");
                        writer.WriteLine($"/// <param name=\"principalType\">The type of the principal to assign to.</param>");
                        writer.WriteLine($"/// <param name=\"principalId\">The principal to assign to.</param>");
                        writer.WriteLine($"/// <param name=\"bicepIdentifierSuffix\">Optional role assignment identifier name suffix.</param>");
                        writer.WriteLine($"/// <returns>The <see cref=\"RoleAssignment\"/>.</returns>");
                        writer.WriteLine($"public RoleAssignment CreateRoleAssignment({Spec!.Name}BuiltInRole role, BicepValue<RoleManagementPrincipalType> principalType, BicepValue<Guid> principalId, string? bicepIdentifierSuffix = default) =>");
                        using (writer.Scope())
                        {
                            writer.WriteLine($"new($\"{{BicepIdentifier}}_{{{Spec!.Name}BuiltInRole.GetBuiltInRoleName(role)}}{{(bicepIdentifierSuffix is null ? \"\" : \"_\")}}{{bicepIdentifierSuffix}}\")");
                            using (writer.Scope("{", "};"))
                            {
                                writer.Write($"Name = BicepFunction.CreateGuid(");
                                if (Properties.Any(p => p.Name == "Id")) { writer.Write("Id, "); }
                                writer.WriteLine($"principalId, BicepFunction.GetSubscriptionResourceId(\"Microsoft.Authorization/roleDefinitions\", role.ToString())),");
                                writer.WriteLine($"Scope = new IdentifierExpression(BicepIdentifier),");
                                writer.WriteLine($"PrincipalType = principalType,");
                                writer.WriteLine($"RoleDefinitionId = BicepFunction.GetSubscriptionResourceId(\"Microsoft.Authorization/roleDefinitions\", role.ToString()),");
                                writer.WriteLine($"PrincipalId = principalId");
                            }
                        }
                    }
                }

                // Write out the model
                Spec!.SaveFile($"{Name}.cs", writer.ToString());
            });
    }
}
